Raphael.fn.connection = function (obj1, obj2, line, bg) {
    if (obj1.line && obj1.from && obj1.to) {
        line = obj1;
        obj1 = line.from;
        obj2 = line.to;
    }
    var bb1 = obj1.getBBox(),
        bb2 = obj2.getBBox(),
        p = [{ x: bb1.x + bb1.width / 2, y: bb1.y - 1 },
        { x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + 1 },
        { x: bb1.x - 1, y: bb1.y + bb1.height / 2 },
        { x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height / 2 },
        { x: bb2.x + bb2.width / 2, y: bb2.y - 1 },
        { x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + 1 },
        { x: bb2.x - 1, y: bb2.y + bb2.height / 2 },
        { x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height / 2}],
        d = {}, dis = [];
    for (var i = 0; i < 4; i++) {
        for (var j = 4; j < 8; j++) {
            var dx = Math.abs(p[i].x - p[j].x),
                dy = Math.abs(p[i].y - p[j].y);
            if ((i == j - 4) || (((i != 3 && j != 6) || p[i].x < p[j].x) && ((i != 2 && j != 7) || p[i].x > p[j].x) && ((i != 0 && j != 5) || p[i].y > p[j].y) && ((i != 1 && j != 4) || p[i].y < p[j].y))) {
                dis.push(dx + dy);
                d[dis[dis.length - 1]] = [i, j];
            }
        }
    }
    if (dis.length == 0) {
        var res = [0, 4];
    } else {
        res = d[Math.min.apply(Math, dis)];
    }
    var x1 = p[res[0]].x,
        y1 = p[res[0]].y,
        x4 = p[res[1]].x,
        y4 = p[res[1]].y;
    dx = Math.max(Math.abs(x1 - x4) / 2, 10);
    dy = Math.max(Math.abs(y1 - y4) / 2, 10);
    var x2 = [x1, x1, x1 - dx, x1 + dx][res[0]].toFixed(3),
        y2 = [y1 - dy, y1 + dy, y1, y1][res[0]].toFixed(3),
        x3 = [0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx][res[1]].toFixed(3),
        y3 = [0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4][res[1]].toFixed(3);
    var path = ["M", x1.toFixed(3), y1.toFixed(3), "C", x2, y2, x3, y3, x4.toFixed(3), y4.toFixed(3)].join(",");
    if (line && line.line) {
        line.bg && line.bg.attr({ path: path });
        line.line.attr({ path: path });
    } else {
        var color = typeof line == "string" ? line : "#000";
        return {
            bg: bg && bg.split && this.path(path).attr({ stroke: bg.split("|")[0], fill: "none", "stroke-width": bg.split("|")[1] || 3 }),
            line: this.path(path).attr({ stroke: color, fill: "none" }),
            from: obj1,
            to: obj2
        };
    }
};


//var el;
//window.onload = function () {
//    var dragger = function () {
//        this.ox = this.type == "rect" ? this.attr("x") : this.attr("cx");
//        this.oy = this.type == "rect" ? this.attr("y") : this.attr("cy");
//        this.animate({"fill-opacity": .2}, 500);
//    },
//        move = function (dx, dy) {
//            var att = this.type == "rect" ? {x: this.ox + dx, y: this.oy + dy} : {cx: this.ox + dx, cy: this.oy + dy};
//            this.attr(att);
//            for (var i = connections.length; i--;) {
//                r.connection(connections[i]);
//            }
//            r.safari();
//        },
//        up = function () {
//            this.animate({"fill-opacity": 0}, 500);
//        },
//        r = Raphael("holder", 640, 480),
//        connections = [],
//        shapes = [  r.ellipse(190, 100, 30, 20),
//                    r.rect(290, 80, 60, 40, 10),
//                    r.rect(290, 180, 60, 40, 2),
//                    r.ellipse(450, 100, 20, 20),
//                    r.ellipse(550, 100, 20, 20)
//                ];
//    for (var i = 0, ii = shapes.length; i < ii; i++) {
//        var color = Raphael.getColor();
//        shapes[i].attr({fill: color, stroke: color, "fill-opacity": 0, "stroke-width": 2, cursor: "move"});
//        shapes[i].drag(move, dragger, up);
//    }
//    connections.push(r.connection(shapes[0], shapes[1], "#fff"));
//    connections.push(r.connection(shapes[1], shapes[2], "#fff", "#fff|5"));
//    connections.push(r.connection(shapes[1], shapes[3], "#000", "#fff"));
//    connections.push(r.connection(shapes[3], shapes[4], "#000", "#fff|5"));
//};



function FFF(paper) {

    var Key = {
        BACKSPACE: 8,
        TAB: 9,
        ENTER: 13,
        SHIFT: 16,
        CONTROL: 17,
        CAPS_LOCK: 20,
        ESCAPE: 27,
        SPACE: 32,
        PAGE_UP: 33,
        PAGE_DOWN: 34,
        END: 35,
        HOME: 36,
        LEFT: 37,
        UP: 38,
        RIGHT: 39,
        DOWN: 40,
        INSERT: 45,
        DELETE: 46,
        NUMPAD_MULTIPLY: 106,
        NUMPAD_ADD: 107,
        NUMPAD_ENTER: 108,
        NUMPAD_SUBTRACT: 109,
        NUMPAD_DECIMAL: 110,
        NUMPAD_DIVIDE: 111,
        COMMA: 188,
        PERIOD: 190
    };

    var background = paper
    .rect(0, 0, 5000, 5000)	// Opera can't hande 10000, 10000!!
    .attr({ fill: "#000", "fill-opacity": 0, "stroke-width": 0 });

    paper.TextEdit = function (x, y, value) {
        // for left alignment: 'text-anchor': 'start'
        var text = this.text(x, y, value).attr({ "font-size": 16 });
        text.textEdit = text;
        text.background = paper.rect().
      attr({ stroke: "#000", fill: "#DDD", "fill-opacity": 0, "stroke-width": 0, "stroke-dasharray": "-" });
        text.cursor = paper.rect().attr({ fill: "#88F", stroke: "#FFF", "stroke-width": 0 }).hide();

        var x_offset = $.browser.msie ? -3 : -6;
        var y_offset = $.browser.msie ? -4 : -1;
        var x_margin = $.browser.msie ? 8 : 10;
        var y_margin = 3;
        if ($.browser.opera) { y_offset = -4; y_margin = 6; }
        text.x_offset = x_offset;
        text.y_offset = y_offset;
        text.x_margin = x_margin;
        text.y_margin = y_margin;

        text.resized = function () { };
        text.value = function () {
            return this.attr('text');
        };
        text.setText = function (str) {
            if (str === '') {
                str = '_';
                text.cursorLeft = 0;
                text.cursorRight = 1;
            }
            text.attr({ text: str });
            var bbox = text.getBBox();
            this.background.attr({
                x: bbox.x + x_offset, y: bbox.y + y_offset,
                width: bbox.width + x_margin, height: bbox.height + y_margin
            });
            // Force the cursor inside the new length
            if (this.cursorRight > str.length)
                this.cursorRight = str.length;
            if (this.cursorLeft > this.cursorRight)
                this.cursorLeft = this.cursorRight;
            this.reshapeCursor();
            this.resized();
        };

        /*--------------- Cursor ---------------*/
        text.cursorLeft = 0;    // character position of first selected character
        text.cursorRight = 0;   // character position after last selected character
        text.reshapeCursor = function () {
            var bb = this.getBBox();
            var x1 = text.widthToCharNum(text.cursorLeft);
            var x2 = text.widthToCharNum(text.cursorRight);
            text.cursor.attr({
                x: bb.x + x1 + x_offset + 4, y: bb.y + y_offset + 2,
                width: (x2 == x1 ? 2 : x2 - x1 + 2), height: bb.height + text.y_margin - 4
            });
        };
        text.positionCursor = function (charNum) {
            var charCount = text.attr("text").length;
            if (charNum > charCount)
                charNum = charCount;
            else if (charNum < 0)
                charNum = 0;
            this.cursorLeft = this.cursorRight = charNum;
            this.reshapeCursor();
        };
        text.selectAll = function () {
            text.cursorLeft = 0;
            text.cursorRight = text.attr("text").length;
            text.select();
            text.reshapeCursor();
        };

        /*--------------- Keyboard input ---------------*/
        text.handleKey = function (k, e) {
            var str = text.attr("text");
            // log("text.handleKey, code="+k.code+", str="+k.str);
            //log("text handling keyCode="+k.code);
            if (k.str) {
                text.setText(str.slice(0, text.cursorLeft) + k.str + str.slice(text.cursorRight));
                text.cursorLeft++;
                text.cursorRight = text.cursorLeft;
                text.reshapeCursor();
            } else {
                var newtext;
                switch (k.code) {
                    case Key.ENTER:  // Enter
                        text.deselect();  // This only works to edit single-line text
                        break;
                    case Key.LEFT:  // cursor left
                        if (e.shiftKey) {
                            if (text.cursorLeft > 0) { text.cursorLeft--; }
                        } else {
                            text.cursorRight = text.cursorLeft;
                            if (text.cursorLeft > 0) { text.cursorLeft--; text.cursorRight--; }
                        }
                        text.reshapeCursor();
                        break;
                    case Key.RIGHT:  // cursor right
                        if (e.shiftKey) {
                            if (text.cursorRight < str.length) { text.cursorRight++; }
                        } else {
                            text.cursorLeft = text.cursorRight;
                            if (text.cursorLeft < str.length) { text.cursorLeft++; text.cursorRight++; }
                        }
                        text.reshapeCursor();
                        break;
                    case Key.DELETE:  // Delete forwards
                        if (text.cursorLeft == text.cursorRight && text.cursorRight < str.length) { text.cursorRight++; }
                        newtext = str.slice(0, text.cursorLeft) + str.slice(text.cursorRight);
                        text.cursorRight = text.cursorLeft;
                        text.setText(newtext);
                        text.reshapeCursor();
                        break;
                    case Key.BACKSPACE:   // Backspace
                        if (text.cursorLeft == text.cursorRight && text.cursorLeft > 0) { text.cursorLeft--; }
                        newtext = str.slice(0, text.cursorLeft) + str.slice(text.cursorRight);
                        text.cursorRight = text.cursorLeft;
                        text.setText(newtext);
                        text.reshapeCursor();
                        break;
                    case Key.END:
                        text.cursorLeft = text.cursorRight = str.length;
                        text.reshapeCursor();
                        break;
                    case Key.HOME:
                        text.cursorLeft = text.cursorRight = 0;
                        text.reshapeCursor();
                        break;
                    case Key.SHIFT:   // Shift
                        return true;   // Propagate the key
                    default:
                        log("unhandled special keyCode=" + k.code);
                        return true;    // The key was not handled
                }
            }
            return false;  // The key was handled
        };

        /*--------------- Selection ---------------*/
        text.isSelected = function () {
            return paper.isSelected(this);
        };
        text.deselect = function () {
            if (this.activated)
                this.deactivate();
            this.background.attr({ "fill-opacity": 0, "stroke-width": 0 });
            this.cursor.hide();
            paper.deselect(this);
            paper.revertFocus();
        };
        text.select = function (event) {
            if (event) {
                var position = paper.pageOffset();
                var found = text.locate({ x: event.pageX - position.left, y: event.pageY - position.top });
                text.cursorLeft = text.cursorRight = found.charNum;
                text.reshapeCursor();
            } else {
                text.cursor.show();
                text.background.attr({ "fill-opacity": 1, "stroke-width": 1 });
            }
            paper.select(text);
            paper.takeFocus(this);
        };
        text.extendCursor = function (event) {
            var position = paper.pageOffset();
            var found = text.locate({ x: event.pageX - position.left, y: event.pageY - position.top });
            if (text.cursorLeft >= found.charNum) {
                // Because we don't remember the original start location, this yields slightly assymetric results.
                // For example, drag left, then right again - it's as if you started from where you went left to.
                text.cursorLeft = found.charNum;
            } else {
                text.cursorRight = found.charNum;
            }
            text.reshapeCursor();
        };

        /*--------------- Live and Die together ---------------*/
        text._real_translate = text.translate;
        text.translate = function (dx, dy) {
            this._real_translate(dx, dy);
            text.background.translate(dx, dy);
            text.cursor.translate(dx, dy);
        };
        text._real_toFront = text.toFront;
        text.toFront = function () {
            text.background.toFront();
            text.cursor.toFront();
            this._real_toFront();
        };
        text._real_toBack = text.toBack;
        text.toBack = function () {
            this._real_toBack();
            text.cursor.toBack();
            text.background.toBack();
            background.toBack();
        };
        text._real_remove = text.remove;
        text.remove = function () {
            paper.revertFocus(this);
            text.background.remove();
            text.cursor.remove();
            // Unregister event bindings?
            text._real_remove();
        };

        /*--------------- Object highlighting ---------------*/
        text.highlight = function () {
            text.background.attr({ "fill-opacity": 1 });
        };
        text.unhighlight = function () {
            if (!text.isSelected())
                text.background.attr({ "fill-opacity": 0 });
        };
        text.activate = function () {
            // Enter text-edit mode with all text selected
            paper.deselectAll();
            text.selectAll();
            text.activated = true;
        };
        text.deactivate = function () {
            text.activated = false;
        };

        /*--------------- Find character position from click ---------------*/
        text.widthToCharNum = function (charNum) {
            var charCount = text.attr("text").length;
            if (charNum > charCount)
                charNum = charCount;
            else if (charNum <= 0)
                return 0;
            if (window.Raphael.svg) {
                try {
                    // log('count='+charCount+" num="+charNum);
                    return text.node.getSubStringLength(0, charNum);
                } catch (e) {
                    return text.widthToCharNum(charNum - 1);
                }
            } else {	// MSIE
                if (charNum === 0)
                    return 0; // First char
                // REVISIT: Can't do this yet on MSIE; use fixed-width assumption
                var bb = this.getBBox();
                return bb.width * charNum / charCount;
            }
        };
        text.posFromCharNum = function (charNum) {
            var bbox = this.getBBox();
            return bbox.x + text.widthToCharNum(charNum);
        };
        text.locate = function (pos) {
            var bbox = this.getBBox();
            var str = text.attr("text");
            var last = 0;
            var charNum = -1;
            for (var i = 0; i < str.length; i++) {
                var width = text.widthToCharNum(i);
                if (pos.x >= bbox.x + last && pos.x <= bbox.x + width) {
                    if (pos.x - bbox.x - last > (width - last) / 2) {
                        charNum = i;
                    } else {
                        charNum = i - 1;
                    }
                    break;
                }
                last = width;
            }
            if (charNum < 0) {
                if (pos.x < bbox.x) { charNum = 0; } else { charNum = str.length; }
            }
            var x = text.widthToCharNum(charNum) - 1;
            return { x: x, top: 0, bottom: bbox.height, charNum: charNum };
        };

        /*--------------- Event Handlers ---------------*/
        text.draggable({ reluctance: 0 });
        text.background.draggable({ reluctance: 0 });
        text.dragStart = function (x, y, downEvent, moveEvent) {
            if (!text.activated) return null;
            downEvent = $.event.fix(downEvent);
            text.select(downEvent);
            return {
                dragUpdate: function (dragging_over, dx, dy, event) {
                    text.extendCursor(event);
                },
                dragFinish: function () { },
                hide: function () { },
                show: function () { }
            };
        };
        text.background.dragStart = text.dragStart;

        text.dblclick(function (event) { text.activate(); return false; });
        text.background.dblclick(function (event) { text.activate(); return false; });

        text.hover(function () { text.highlight(); }, function () { text.unhighlight(); });
        text.background.hover(function () { text.highlight(); }, function () { text.unhighlight(); });

        text.setText(value);
        text.deselect();
        text.toFront();
        return text;
    };

    var selected_objects = [];
    paper.setSelection = function (obj) {
        this.deselectAll();
        this.select(obj);
    };

    paper.isSelected = function (obj) {
        for (var i in selected_objects)
            if (selected_objects[i] == obj) return true;
        return false;
    };

    paper.select = function (obj) {
        // Should be able to filter the different kinds of things selected.
        if (!paper.isSelected(obj)) {       // Don't select anything more than once!
            selected_objects.push(obj);
        }
    };

    paper.deselect = function (obj) {
        for (var i in selected_objects)
            if (selected_objects[i] == obj)
                selected_objects.splice(i, 1);
    };

    paper.deselectAll = function () {
        while (selected_objects.length > 0)
            selected_objects[0].deselect();
    };

    paper.revertFocus = function (obj) {
        if (!obj || obj === paper.keyboardFocus)
            paper.keyboardFocus = paper;
    };

    paper.takeFocus = function (obj) {
        paper.keyboardFocus = obj;
    };
    paper.hasFocus = function (obj) {
        paper.keyboardFocus === obj;
    };

    paper.pageOffset = function () {
        return $.browser.opera ? $(this.canvas.parentNode).offset() : $(this.canvas).offset();
    };

    var downKey;

    /* Handle special keys using keydown; these browsers don't send keypress for these events */
    if ($.browser.msie || $.browser.safari) {
        // Handle special keys using keydown; these browsers don't send keypress for these events
        $(document).bind('keydown', function (e) {
            if (downKey && downKey == e.keyCode)    // Auto-repeat
                paper.keyboardFocus.handleKey({ code: downKey }, e);
            downKey = e.keyCode;
            if ((e.charCode || e.which) == 8) return false; /* Don't go to the previous page */
            return true;
        });
        $(document).bind('keyup', function (e) {
            if (downKey)  // There was no keypress in between; must be a special key
                paper.keyboardFocus.handleKey({ code: downKey }, e);
            downKey = null;
            return true;
        });
    }

    if (false && jQuery.browser.safari) {
        // REVISIT: Need to handle non-text keys below, and cut/copy/paste; but not double-handle text below
        $(document).live('textInput', function (e) {
            return paper.keyboardFocus.handleKey({ str: e.originalEvent.data }, e);
        });
    } else {
        $(document).bind('keypress', function (e) {
            var charCode = e.charCode || e.which;
            downKey = null;
            if ($.browser.opera) {
                if (!e.originalEvent.which)   // Without this, left-arrow and single-quote are identical
                    charCode = null;
                e.ctrlKey = e.metaKey;   // Opera also sets metaKey instead of ctrlKey :-(
            } else if ($.browser.msie)
                charCode = e.which;
            else
                charCode = e.charCode;
            if (charCode && !e.metaKey && !e.altKey && !e.ctrlKey && charCode >= 32)
                return paper.keyboardFocus.handleKey({ str: String.fromCharCode(charCode) }, e);
            else  // control key on MSIE, or any special key on Firefox, etc
                return paper.keyboardFocus.handleKey({ code: e.keyCode }, e);
        });
    }
};